Skip to content

Fix double free in DecompressedData init failure path (DCTAnimationCacheImpl)#2221

Open
ankuper wants to merge 1 commit into
TelegramMessenger:masterfrom
ankuper:fix-decompresseddata-double-free
Open

Fix double free in DecompressedData init failure path (DCTAnimationCacheImpl)#2221
ankuper wants to merge 1 commit into
TelegramMessenger:masterfrom
ankuper:fix-decompresseddata-double-free

Conversation

@ankuper

@ankuper ankuper commented Jul 3, 2026

Copy link
Copy Markdown

Why

DecompressedData.init?(compressedData:dataRange:) in DCTAnimationCacheImpl.swift allocates self.stream and, if compression_stream_init fails, manually deallocates it before returning nil:

self.stream = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
guard compression_stream_init(self.stream, COMPRESSION_STREAM_DECODE, algorithm) != COMPRESSION_STATUS_ERROR else {
    self.stream.deallocate()
    return nil
}

By the point return nil executes, every stored property (compressedData, dataRange, and stream) has already been assigned. Under Swift's documented behavior for failable initializers, once all stored properties are set, deinit still runs on the instance even though init? returned nil. deinit unconditionally does:

deinit {
    compression_stream_destroy(self.stream)
    self.stream.deallocate()
}

So self.stream gets deallocated twice: once explicitly in the init? failure branch, and again from deinit. compression_stream_init zeroes the stream's non-state fields per its own documentation, but that doesn't change the fact that the outer UnsafeMutablePointer<compression_stream> allocation itself is freed twice — a double free.

Crash

Observed on-device on iOS 26.5 (23F77), SIGABRT, libsystem_malloc.dylib reporting:

___BUG_IN_CLIENT_OF_LIBMALLOC_POINTER_BEING_FREED_WAS_NOT_ALLOCATED

Faulting thread was on queue DCTMultiAnimationRenderer-FirstFrame, unwinding through swift_release_deallocDecompressedData.__deallocating_deinitDecompressedData.deinit, with DecompressedData.init(compressedData:dataRange:) further up the same call chain — consistent with deinit running a second time on the stream pointer that init?'s failure branch had already freed.

Fix

Remove the manual self.stream.deallocate() from the init? failure branch. deinit already destroys and deallocates self.stream unconditionally, and per Swift's ARC semantics it will run exactly once for this instance regardless of whether init? returned nil (once stored properties are set) or completed successfully. This makes deinit the single owner of that cleanup, eliminating the double free.

Testing

  • Verified via crash log symbolication (atos against the release dSYM) that the pre-fix code path matches this exact double-free mechanism.
  • Confirmed the buggy pattern is unchanged on current master (not something we introduced downstream).
  • Change is a 1-line deletion (plus explanatory comment); no behavior change on the success path.

…failed init

compression_stream_init() failure was manually deallocating self.stream
before returning nil. Since all stored properties are already assigned
by that point, Swift still calls deinit on the failed-init instance,
which unconditionally destroys+deallocates self.stream again — a
double free that libsystem_malloc catches as "pointer being freed was
not allocated" (SIGABRT, DCTMultiAnimationRenderer-FirstFrame queue,
build 33197). Let deinit be the sole owner of that cleanup.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant